/*
 * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

/*
 * (C) Copyright IBM Corp. 1999,  All rights reserved.
 */
package java.awt.font;

import java.awt.Font;
import java.awt.Toolkit;
import java.awt.im.InputMethodHighlight;
import java.text.Annotation;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.util.Vector;
import java.util.HashMap;
import java.util.Map;
import sun.font.Decoration;
import sun.font.FontResolver;
import sun.text.CodePointIterator;

/**
 * This class stores Font, GraphicAttribute, and Decoration intervals
 * on a paragraph of styled text.
 * <p>
 * Currently, this class is optimized for a small number of intervals
 * (preferrably 1).
 */
final class StyledParagraph {

  // the length of the paragraph
  private int length;

  // If there is a single Decoration for the whole paragraph, it
  // is stored here.  Otherwise this field is ignored.

  private Decoration decoration;

  // If there is a single Font or GraphicAttribute for the whole
  // paragraph, it is stored here.  Otherwise this field is ignored.
  private Object font;

  // If there are multiple Decorations in the paragraph, they are
  // stored in this Vector, in order.  Otherwise this vector and
  // the decorationStarts array are null.
  private Vector<Decoration> decorations;
  // If there are multiple Decorations in the paragraph,
  // decorationStarts[i] contains the index where decoration i
  // starts.  For convenience, there is an extra entry at the
  // end of this array with the length of the paragraph.
  int[] decorationStarts;

  // If there are multiple Fonts/GraphicAttributes in the paragraph,
  // they are
  // stored in this Vector, in order.  Otherwise this vector and
  // the fontStarts array are null.
  private Vector<Object> fonts;
  // If there are multiple Fonts/GraphicAttributes in the paragraph,
  // fontStarts[i] contains the index where decoration i
  // starts.  For convenience, there is an extra entry at the
  // end of this array with the length of the paragraph.
  int[] fontStarts;

  private static int INITIAL_SIZE = 8;

  /**
   * Create a new StyledParagraph over the given styled text.
   *
   * @param aci an iterator over the text
   * @param chars the characters extracted from aci
   */
  public StyledParagraph(AttributedCharacterIterator aci,
      char[] chars) {

    int start = aci.getBeginIndex();
    int end = aci.getEndIndex();
    length = end - start;

    int index = start;
    aci.first();

    do {
      final int nextRunStart = aci.getRunLimit();
      final int localIndex = index - start;

      Map<? extends Attribute, ?> attributes = aci.getAttributes();
      attributes = addInputMethodAttrs(attributes);
      Decoration d = Decoration.getDecoration(attributes);
      addDecoration(d, localIndex);

      Object f = getGraphicOrFont(attributes);
      if (f == null) {
        addFonts(chars, attributes, localIndex, nextRunStart - start);
      } else {
        addFont(f, localIndex);
      }

      aci.setIndex(nextRunStart);
      index = nextRunStart;

    } while (index < end);

    // Add extra entries to starts arrays with the length
    // of the paragraph.  'this' is used as a dummy value
    // in the Vector.
    if (decorations != null) {
      decorationStarts = addToVector(this, length, decorations, decorationStarts);
    }
    if (fonts != null) {
      fontStarts = addToVector(this, length, fonts, fontStarts);
    }
  }

  /**
   * Adjust indices in starts to reflect an insertion after pos.
   * Any index in starts greater than pos will be increased by 1.
   */
  private static void insertInto(int pos, int[] starts, int numStarts) {

    while (starts[--numStarts] > pos) {
      starts[numStarts] += 1;
    }
  }

  /**
   * Return a StyledParagraph reflecting the insertion of a single character
   * into the text.  This method will attempt to reuse the given paragraph,
   * but may create a new paragraph.
   *
   * @param aci an iterator over the text.  The text should be the same as the text used to create
   * (or most recently update) oldParagraph, with the exception of inserting a single character at
   * insertPos.
   * @param chars the characters in aci
   * @param insertPos the index of the new character in aci
   * @param oldParagraph a StyledParagraph for the text in aci before the insertion
   */
  public static StyledParagraph insertChar(AttributedCharacterIterator aci,
      char[] chars,
      int insertPos,
      StyledParagraph oldParagraph) {

    // If the styles at insertPos match those at insertPos-1,
    // oldParagraph will be reused.  Otherwise we create a new
    // paragraph.

    char ch = aci.setIndex(insertPos);
    int relativePos = Math.max(insertPos - aci.getBeginIndex() - 1, 0);

    Map<? extends Attribute, ?> attributes =
        addInputMethodAttrs(aci.getAttributes());
    Decoration d = Decoration.getDecoration(attributes);
    if (!oldParagraph.getDecorationAt(relativePos).equals(d)) {
      return new StyledParagraph(aci, chars);
    }
    Object f = getGraphicOrFont(attributes);
    if (f == null) {
      FontResolver resolver = FontResolver.getInstance();
      int fontIndex = resolver.getFontIndex(ch);
      f = resolver.getFont(fontIndex, attributes);
    }
    if (!oldParagraph.getFontOrGraphicAt(relativePos).equals(f)) {
      return new StyledParagraph(aci, chars);
    }

    // insert into existing paragraph
    oldParagraph.length += 1;
    if (oldParagraph.decorations != null) {
      insertInto(relativePos,
          oldParagraph.decorationStarts,
          oldParagraph.decorations.size());
    }
    if (oldParagraph.fonts != null) {
      insertInto(relativePos,
          oldParagraph.fontStarts,
          oldParagraph.fonts.size());
    }
    return oldParagraph;
  }

  /**
   * Adjust indices in starts to reflect a deletion after deleteAt.
   * Any index in starts greater than deleteAt will be increased by 1.
   * It is the caller's responsibility to make sure that no 0-length
   * runs result.
   */
  private static void deleteFrom(int deleteAt, int[] starts, int numStarts) {

    while (starts[--numStarts] > deleteAt) {
      starts[numStarts] -= 1;
    }
  }

  /**
   * Return a StyledParagraph reflecting the insertion of a single character
   * into the text.  This method will attempt to reuse the given paragraph,
   * but may create a new paragraph.
   *
   * @param aci an iterator over the text.  The text should be the same as the text used to create
   * (or most recently update) oldParagraph, with the exception of deleting a single character at
   * deletePos.
   * @param chars the characters in aci
   * @param deletePos the index where a character was removed
   * @param oldParagraph a StyledParagraph for the text in aci before the insertion
   */
  public static StyledParagraph deleteChar(AttributedCharacterIterator aci,
      char[] chars,
      int deletePos,
      StyledParagraph oldParagraph) {

    // We will reuse oldParagraph unless there was a length-1 run
    // at deletePos.  We could do more work and check the individual
    // Font and Decoration runs, but we don't right now...
    deletePos -= aci.getBeginIndex();

    if (oldParagraph.decorations == null && oldParagraph.fonts == null) {
      oldParagraph.length -= 1;
      return oldParagraph;
    }

    if (oldParagraph.getRunLimit(deletePos) == deletePos + 1) {
      if (deletePos == 0 || oldParagraph.getRunLimit(deletePos - 1) == deletePos) {
        return new StyledParagraph(aci, chars);
      }
    }

    oldParagraph.length -= 1;
    if (oldParagraph.decorations != null) {
      deleteFrom(deletePos,
          oldParagraph.decorationStarts,
          oldParagraph.decorations.size());
    }
    if (oldParagraph.fonts != null) {
      deleteFrom(deletePos,
          oldParagraph.fontStarts,
          oldParagraph.fonts.size());
    }
    return oldParagraph;
  }

  /**
   * Return the index at which there is a different Font, GraphicAttribute, or
   * Dcoration than at the given index.
   *
   * @param index a valid index in the paragraph
   * @return the first index where there is a change in attributes from those at index
   */
  public int getRunLimit(int index) {

    if (index < 0 || index >= length) {
      throw new IllegalArgumentException("index out of range");
    }
    int limit1 = length;
    if (decorations != null) {
      int run = findRunContaining(index, decorationStarts);
      limit1 = decorationStarts[run + 1];
    }
    int limit2 = length;
    if (fonts != null) {
      int run = findRunContaining(index, fontStarts);
      limit2 = fontStarts[run + 1];
    }
    return Math.min(limit1, limit2);
  }

  /**
   * Return the Decoration in effect at the given index.
   *
   * @param index a valid index in the paragraph
   * @return the Decoration at index.
   */
  public Decoration getDecorationAt(int index) {

    if (index < 0 || index >= length) {
      throw new IllegalArgumentException("index out of range");
    }
    if (decorations == null) {
      return decoration;
    }
    int run = findRunContaining(index, decorationStarts);
    return decorations.elementAt(run);
  }

  /**
   * Return the Font or GraphicAttribute in effect at the given index.
   * The client must test the type of the return value to determine what
   * it is.
   *
   * @param index a valid index in the paragraph
   * @return the Font or GraphicAttribute at index.
   */
  public Object getFontOrGraphicAt(int index) {

    if (index < 0 || index >= length) {
      throw new IllegalArgumentException("index out of range");
    }
    if (fonts == null) {
      return font;
    }
    int run = findRunContaining(index, fontStarts);
    return fonts.elementAt(run);
  }

  /**
   * Return i such that starts[i] &lt;= index &lt; starts[i+1].  starts
   * must be in increasing order, with at least one element greater
   * than index.
   */
  private static int findRunContaining(int index, int[] starts) {

    for (int i = 1; true; i++) {
      if (starts[i] > index) {
        return i - 1;
      }
    }
  }

  /**
   * Append the given Object to the given Vector.  Add
   * the given index to the given starts array.  If the
   * starts array does not have room for the index, a
   * new array is created and returned.
   */
  @SuppressWarnings({"rawtypes", "unchecked"})
  private static int[] addToVector(Object obj,
      int index,
      Vector v,
      int[] starts) {

    if (!v.lastElement().equals(obj)) {
      v.addElement(obj);
      int count = v.size();
      if (starts.length == count) {
        int[] temp = new int[starts.length * 2];
        System.arraycopy(starts, 0, temp, 0, starts.length);
        starts = temp;
      }
      starts[count - 1] = index;
    }
    return starts;
  }

  /**
   * Add a new Decoration run with the given Decoration at the
   * given index.
   */
  private void addDecoration(Decoration d, int index) {

    if (decorations != null) {
      decorationStarts = addToVector(d,
          index,
          decorations,
          decorationStarts);
    } else if (decoration == null) {
      decoration = d;
    } else {
      if (!decoration.equals(d)) {
        decorations = new Vector<Decoration>(INITIAL_SIZE);
        decorations.addElement(decoration);
        decorations.addElement(d);
        decorationStarts = new int[INITIAL_SIZE];
        decorationStarts[0] = 0;
        decorationStarts[1] = index;
      }
    }
  }

  /**
   * Add a new Font/GraphicAttribute run with the given object at the
   * given index.
   */
  private void addFont(Object f, int index) {

    if (fonts != null) {
      fontStarts = addToVector(f, index, fonts, fontStarts);
    } else if (font == null) {
      font = f;
    } else {
      if (!font.equals(f)) {
        fonts = new Vector<Object>(INITIAL_SIZE);
        fonts.addElement(font);
        fonts.addElement(f);
        fontStarts = new int[INITIAL_SIZE];
        fontStarts[0] = 0;
        fontStarts[1] = index;
      }
    }
  }

  /**
   * Resolve the given chars into Fonts using FontResolver, then add
   * font runs for each.
   */
  private void addFonts(char[] chars, Map<? extends Attribute, ?> attributes,
      int start, int limit) {

    FontResolver resolver = FontResolver.getInstance();
    CodePointIterator iter = CodePointIterator.create(chars, start, limit);
    for (int runStart = iter.charIndex(); runStart < limit; runStart = iter.charIndex()) {
      int fontIndex = resolver.nextFontRunIndex(iter);
      addFont(resolver.getFont(fontIndex, attributes), runStart);
    }
  }

  /**
   * Return a Map with entries from oldStyles, as well as input
   * method entries, if any.
   */
  static Map<? extends Attribute, ?>
  addInputMethodAttrs(Map<? extends Attribute, ?> oldStyles) {

    Object value = oldStyles.get(TextAttribute.INPUT_METHOD_HIGHLIGHT);

    try {
      if (value != null) {
        if (value instanceof Annotation) {
          value = ((Annotation) value).getValue();
        }

        InputMethodHighlight hl;
        hl = (InputMethodHighlight) value;

        Map<? extends Attribute, ?> imStyles = null;
        try {
          imStyles = hl.getStyle();
        } catch (NoSuchMethodError e) {
        }

        if (imStyles == null) {
          Toolkit tk = Toolkit.getDefaultToolkit();
          imStyles = tk.mapInputMethodHighlight(hl);
        }

        if (imStyles != null) {
          HashMap<Attribute, Object>
              newStyles = new HashMap<>(5, (float) 0.9);
          newStyles.putAll(oldStyles);

          newStyles.putAll(imStyles);

          return newStyles;
        }
      }
    } catch (ClassCastException e) {
    }

    return oldStyles;
  }

  /**
   * Extract a GraphicAttribute or Font from the given attributes.
   * If attributes does not contain a GraphicAttribute, Font, or
   * Font family entry this method returns null.
   */
  private static Object getGraphicOrFont(
      Map<? extends Attribute, ?> attributes) {

    Object value = attributes.get(TextAttribute.CHAR_REPLACEMENT);
    if (value != null) {
      return value;
    }
    value = attributes.get(TextAttribute.FONT);
    if (value != null) {
      return value;
    }

    if (attributes.get(TextAttribute.FAMILY) != null) {
      return Font.getFont(attributes);
    } else {
      return null;
    }
  }
}
